package com.em.tuppence.web.security.openid;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.openid.AuthenticationCancelledException;
import org.springframework.security.openid.OpenIDAttribute;
import org.springframework.security.openid.OpenIDAuthenticationProvider;
import org.springframework.security.openid.OpenIDAuthenticationStatus;
import org.springframework.security.openid.OpenIDAuthenticationToken;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import com.em.tuppence.core.model.application.User;
import com.em.tuppence.web.security.details.TuppenceUserDetails;

public class AttributeAwareOpenIDProvider extends OpenIDAuthenticationProvider {

	private UserDetailsService userDetailsService;
	private static final Logger logger = LoggerFactory.getLogger(AttributeAwareOpenIDProvider.class);

	public AttributeAwareOpenIDProvider(final UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
		super.setUserDetailsService(userDetailsService);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see org.springframework.security.providers.openid.OpenIDAuthenticationProvider#authenticate(org.springframework.security.Authentication)
	 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		if (!supports(authentication.getClass())) {
			return null;
		}

		if (authentication instanceof OpenIDAuthenticationToken) {
			OpenIDAuthenticationToken response = (OpenIDAuthenticationToken) authentication;
			OpenIDAuthenticationStatus status = response.getStatus();

			// handle the various possibilites
			if (status == OpenIDAuthenticationStatus.SUCCESS) {
				// Lookup user details
				final UserDetails userDetails;

				try {
					userDetails = this.userDetailsService.loadUserByUsername(response.getIdentityUrl());
				} catch (final UsernameNotFoundException e) {
					//update authentication with authority to see registration page
					Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
					authorities.add(new SimpleGrantedAuthority("ROLE_NEEDS_REGISTRATION_BADLY"));
					SecurityContextHolder.getContext().setAuthentication(
							new PreAuthenticatedAuthenticationToken(
									new TuppenceUserDetails(null),
									"anon-registration-token:" + UUID.randomUUID().toString(), 
									authorities
								)
						);

					AttributeAwareOpenIDProvider.logger.info("OpenID authentication successful but but no account exists.");

					final User user = new User();
					user.setOpenIdUrl(response.getIdentityUrl());

					for (OpenIDAttribute attribute : response.getAttributes()) {

						AttributeAwareOpenIDProvider.logger.debug("OpenIDAttribute: " + attribute.getType() + "; " + attribute.getName() + "; " + attribute.getValues());

						if (UsedOpenIDAttribute.AX_FIRST_NAME.getOpenIdAttribute().getName().equals(attribute.getName())) {
							user.setFirstName(attribute.getValues().get(0));
						} else if (UsedOpenIDAttribute.AX_LAST_NAME.getOpenIdAttribute().getName().equals(attribute.getName())) {
							user.setLastName(attribute.getValues().get(0));
						} else if (UsedOpenIDAttribute.EMAIL.getOpenIdAttribute().getName().equals(attribute.getName())) {
							user.setEmail(attribute.getValues().get(0));
						} else if (UsedOpenIDAttribute.FIRST_NAME.getOpenIdAttribute().getName().equals(attribute.getName()) && user.getFirstName() == null) {
							user.setFirstName(attribute.getValues().get(0));
						} else if (UsedOpenIDAttribute.LAST_NAME.getOpenIdAttribute().getName().equals(attribute.getName()) && user.getLastName() == null) {
							user.setLastName(attribute.getValues().get(0));
						}

					}

					if (StringUtils.isBlank(user.getFirstName()) && StringUtils.isBlank(user.getLastName())) {
						for (OpenIDAttribute attribute : response.getAttributes()) {
							if (UsedOpenIDAttribute.NAME_PERSON.getOpenIdAttribute().getName().equals(attribute.getName())) {
								user.setFirstName(attribute.getValues().get(0));
							}
						}
					}

					throw new AuthenticationSucessButMissingRegistrationException("User is authenticated via OpenID but no account exists, yet.", user);

				}
				
				AttributeAwareOpenIDProvider.logger.info("OpenID user details found: " + userDetails.getUsername());

				return new OpenIDAuthenticationToken(userDetails, userDetails.getAuthorities(), response.getIdentityUrl(), response.getAttributes());
			}

			if (status == OpenIDAuthenticationStatus.CANCELLED) {
				throw new AuthenticationCancelledException("Log in cancelled");
			}

			if (status == OpenIDAuthenticationStatus.ERROR) {
				throw new AuthenticationServiceException("Error message from server: $response.message");
			}

			if (status == OpenIDAuthenticationStatus.FAILURE) {
				throw new BadCredentialsException("Log in failed - identity could not be verified");
			}

			if (status == OpenIDAuthenticationStatus.SETUP_NEEDED) {
				throw new AuthenticationServiceException("The server responded setup was needed, which shouldn't happen");
			}

			throw new AuthenticationServiceException("Unrecognized return value $status");
		}

		return null;
	}

}
